Introduction
Windows Forms is the framework for developing rich client GUI applications under
the .NET framework. There are many cool features in Windows Forms which greatly
simplify development. The problem is that all the cool controls
available at codeproject are written
in MFC and cannot be used on Windows Forms Application directly. I know of at
least three different ways by which existing controls can be migrated to
.NET :-
-
Rewriting the controls completely in managed code
-
Making them ActiveX controls and using the ActiveX control on windows forms
-
Using managed C++.
The purpose of this article is to demonstrate the last of the above techniques.
Demo Control
In order to demonstrate these techniques I select one of my favorite controls in
codeproject Mark C. Malburg's
Analog Meter Control. The aim is to be able to develop a windows forms
control by wrapping the existing MFC control (with little or no changes to
the original MFC code). The final Windows Forms control
developed can be placed on windows forms designer as shown in the image.
The control has following properties :-
Property Name |
Property Type |
Description |
Units |
System::String |
The text of units shown in the meter |
Value |
double |
The value which determines the needle position and also shown at the bottom of
the control |
NeedleColor |
System::Drawing::Color |
The color of the needle |
In addition the control supports a managed event called OnValueChanged
which is fired whenever the Value
property is changed.
To start with we need to create a managed C++ class library with MFC
support. I wrote a wizard that does it. The wizard is downloadable from
http://www.codeproject.com/useritems/ManagedMFCDLL.asp. Download and
install the wizard and also download the source for
Analog Meter Control.
Using Managed C++ With MFC
Our aim is to retain as much existing MFC code as possible because all this code
has been tested and runs well (assuming). VC++.NET allows to mix managed (a
code that is compiled to IL) and unmanaged (native) code. It can even compile
existing code to IL. But you cannot use the classes so compiled in other .NET
languages unless they are marked with <code>__gc
prefix.
e.g <code>__gc
class MyControl . A class that is marked __gc
cannot derive from any class not marked __gc
. This rules out use
of CObject
, CWnd
classes as base class as thet are
not marked __gc
. The other restriction is that __gc
classes
cannot contain members of any (practically) other classes. But a __gc
class can contain a pointer to MFC class.
In order to develop a windows forms control we need to create a class that
extends the managed class (marked with __gc
) System::Windows::Forms::Control
class just like an MFC custom control extends CWnd
. Within
this managed object we will contain an instance of the MFC class. We will make
the implementation of properties and methods of the managed control delegate to
the MFC class and let the MFC code do all the hard work. The problem is that we
need to associate the same window handle (all controls are windows) to both the
MFC object and the System::Windows::Forms::Control
object. This
can be done in two ways :-
-
Let the windows form control create its window an MFC subclass it.
-
Let the windows form control superclass the window used by the MFC control.
Both the methods are covered in the article.
Subclassing Windows Forms control through MFC
We start with creating a blank VS.NET solution with any name.
Add a Managed MFC DLL project to the solution using the wizard named
Control. This project will be used for any managed classes.
Next add to the solution a Win32 static library project with support
for MFC and precompiled headers. Call this ControlS (S stands for static).
This will contain the MFC code for the control. Separation of managed and
unmanaged code makes maintenance a bit easier. We will make the project
"Control" dependent on "ControlS" to link them together.
Place the files 3DMeterCtrl.cpp, 3DMeterCtrl.h and MemDC.h in the "ControlS"
project. Modify the 3DMeterCtrl.cpp file to remove #include
"MeterTestForm.h" as shown
#include "stdafx.h"
#include "math.h"
#include "3DMeterCtrl.h"
#include "MemDC.h"
After this the project ControlS would build successfully.
We need to write a managed wrapper for the control. We call this class
ThreeDMeter and it should derive from System::Windows::Forms::Control
.
In order to do this we need to import the required assemblies add required
#using's to stdafx.h as shown. Any code that needs to be added is shown in blue
#include <afxwin.h> // MFC core and standard components
#include "..\Controls\3DMeterCtrl.h"
#using <mscorlib.dll>
#using <system.drawing.dll>
#using <system.dll>
#using <system.design.dll>
#using <system.windows.forms.dll>
Now we can create the main control class. Replace the default class generated by
the wizard with class ThreeDMeter.
#pragma once
#include "resource.h" // main symbols
using namespace System;
using namespace System::Drawing;
using namespace System::Windows::Forms;
using namespace System::Runtime::InteropServices;
using namespace System::Runtime::Remoting::Messaging;
namespace ControlDemo
{
public __gc class ThreeDMeter : public Control
{
public:
ThreeDMeter()
{
m_pCtrl = new C3DMeterCtrl();
}
protected:
void Dispose(bool b)
{
Control::Dispose(b);
if (m_pCtrl != NULL)
{
delete m_pCtrl;
m_pCtrl = NULL;
}
}
void OnHandleCreated(EventArgs* e)
{
System::Diagnostics::Debug::Assert(m_pCtrl->GetSafeHwnd() == NULL);
m_pCtrl->SubclassWindow((HWND)get_Handle().ToPointer());
Control::OnHandleCreated(e);
}
private:
C3DMeterCtrl* m_pCtrl;
};
}
Compile and build the DLL. At this time we have got a windows forms control that
can be used in a C# or a VB application. To test the control, launch
another instance of VS.NET and create a VB or a VC# Windows Application. Add
the ThreeDMeter control to the toolbox (Click
here if you want to know how to do it). Double clicking on the
ThreeDMeter toolbox item adds the control to the form as shown
If you get any errors try modifying the copy local property of the reference to
Control assembly to false as shown below (I am not sure why this works, I would
be glad if anyone can explain me why setting Copy Local to false makes fixes
the problem)
So we have succeeded in wrapping an MFC control around a managed class and using
it in the windows forms designer. How exactly does it work?
A windows forms control is a window with style of WS_CHILD by default. When a
control is added to the form the window handle is created. When this
happens System::Windows::Forms::Control
's protected method
OnHandleCreated is called. We overload this method and subclass it with the
C3DMeterCtrl object we create, using SubclassWindow method. If you are not
familiar with subclassing refer to
Chris Maunder's tutorial on Subclassing.
Even though our control paints successfully and can be used in the designer
there is not much user can do to modify the behavior of the control. (like
changing the needle color or changing the text of units etc.) In the next step
we would add properties in the control that would allow us to do this.
Lets add code add the properties using managed extensions to C++ keyword
__property. After adding the properties the code looks like this
__property Color get_NeedleColor()
{
if (!m_pCtrl)
throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());
return System::Drawing::ColorTranslator::FromWin32(m_pCtrl->m_colorNeedle);
}
__property void set_NeedleColor(Color clr)
{
if (!m_pCtrl)
throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());
AFX_MANAGE_STATE(AfxGetStaticModuleState());
m_pCtrl->SetNeedleColor(ColorTranslator::ToWin32(clr));
}
__property void set_Units(String* units)
{
if (!m_pCtrl)
throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CString strUnits(units);
m_pCtrl->SetUnits(strUnits);
}
__property String* get_Units()
{
if (!m_pCtrl)
throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());
LPCTSTR szUnits = (m_pCtrl->m_strUnits);
return new String(szUnits);
}
__property double get_Value()
{
if (!m_pCtrl)
throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());
return m_pCtrl->m_dCurrentValue;
}
__property void set_Value(double d)
{
if (!m_pCtrl)
throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());
AFX_MANAGE_STATE(AfxGetStaticModuleState());
m_pCtrl->UpdateNeedle(d);
OnValueChanged(this, EventArgs::Empty);
}
These properties can be called at any time even when the handle is not created.
So we need to make sure that none of the MFC methods that make use of Windows
handle get called before the window is created this would lead MFC to fire
assertions. We fix this in the MFC code in 3DMeterCtrl.cpp as shown
void C3DMeterCtrl::ReconstructControl()
{
if (!GetSafeHwnd())
return;
if ((m_pBitmapOldBackground) &&
(m_bitmapBackground.GetSafeHandle()) &&
(m_dcBackground.GetSafeHdc()))
{
m_dcBackground.SelectObject(m_pBitmapOldBackground);
m_dcBackground.DeleteDC() ;
m_bitmapBackground.DeleteObject();
}
Invalidate () ;
}
The above code demostrates a few points
-
Using properties in C++ (refer to Chris Maunders
tutorial on managed C++ properties for more details).
-
Changing System::Drawing::Color to COLORREF using ColorTranslator
-
Changing System::String type to CString using the new CString constructor
for System::String. (Thanks to Anson Tsao for pointing that to me)
-
Use of AFX_MANAGE_STATE to ensure proper MFC state. Those of you who developed
COM objects using MFC and ATL must be familiar with this.
What we have actually done is to delegate the calls C3DMeterCtrl class. After
you build the project it would be possible to set or change these properties
from the designer and instantly see the change in the rendering of the control
on the VB/VC# form.
It would be really cool if the properties can be organized in the property grid.
This can be done simply by using managed C++ attributes e.g.
[property: System::ComponentModel::CategoryAttribute("Meter")]
__property Color get_NeedleColor()
{
if (!m_pCtrl)
throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());
return System::Drawing::ColorTranslator::FromWin32(m_pCtrl->m_colorNeedle);
}
In the above example we have applied CategoryAttribute
to the property
NeedleColor
. The effect of this attribute can be seen in the property grid
after you build the project with these changes.
The thing which is missing from the Control is that it doesn't fire any events.
An example event could be OnValueChanged
fired when the value changes. The
following declaration indicates that the control supports OnValueChanged
event.
__event EventHandler * OnValueChanged;
In order to fire the event the set_Value
method can be changed as following
__property void set_Value(double d)
{
if (!m_pCtrl)
throw new ObjectDisposedException(__typeof(ThreeDMeter)->ToString());
AFX_MANAGE_STATE(AfxGetStaticModuleState());
m_pCtrl->UpdateNeedle(d);
OnValueChanged(this, EventArgs::Empty);
}
Thus we have a completely functional control based on an MFC control. The
problem with this implementation is that MFC control gets a first shot at the
events and not the managed control. This would be a major problem if the
control needs to be enhanced later on in managed code for example by deriving
another control written C# from this control. This brings us to the next
technique - allowing windows forms to superclass our MFC control so that
windows forms control gets a first shot at the messages.
Allowing Windows Forms control to superclass existing MFC controls
The System::Windows::Forms::Control
class can use an existing
Window Class to create its window. This can be done by overloading
get_CreateParams
method and specifying a new class name. The only restriction
is that Window class should be registered with CS_GLOBALCLASS
. So we register a
new window class in InitInstance
and unregister it in ExitInstance
as shown
BOOL CControlApp::InitInstance()
{
CWinApp::InitInstance();
WNDCLASS wc;
memset(&wc, 0, sizeof(wc));
wc.lpszClassName = "Analog3dMeter";
wc.hInstance = m_hInstance;
wc.lpfnWndProc = Analog3dMeterWindowProc;
wc.style = CS_DBLCLKS | CS_GLOBALCLASS | CS_HREDRAW | CS_VREDRAW;
return RegisterClass(&wc);
}
int CControlApp::ExitInstance()
{
UnregisterClass("Analog3dMeter", m_hInstance);
return CWinApp::ExitInstance();
}
Now we can overload get_CreateParams
method. For demonstration purposes we
create a totally new class called ThreeDMeter2 with much of the code same as
ThreeDMeter
except for the protected methods.
protected:
void Dispose(bool b)
{
Control::Dispose(b);
m_pCtrl = NULL;
}
__property System::Windows::Forms::CreateParams * get_CreateParams()
{
System::Windows::Forms::CreateParams * pParams =
Control::get_CreateParams();
pParams->ClassName = S"Analog3dMeter";
return pParams;
}
Specifying class name as Analog3dMeter makes the window forms control use this
window class to create the window for the control. Now the problem is how do we
associate m_pCtrl
with the window handle so created. This is done at two
places. First the CreateHandle
method of a control which is actually
responsible for creating the window.
void CreateHandle()
{
__try
{
CallContext::SetData(S"Controls.CurrentControl",
__box(IntPtr(m_pCtrl)));
Control::CreateHandle();
}
__finally
{
CallContext::SetData(S"Controls.CurrentControl", NULL);
}
}
CallContext
is an equivalent (kind of) of ThreadLocal
storage here. In the
above code we have set a named property for the thread called
"Controls.CurrentControl
" to the pointer value of
m_pCtrl
. Observe __box
operator which is used to convert a value type to Object* which is the type of
SetData
's second parameter. After setting this property we call base
classes implementation which actually calls CreateWindowEx
and finally we clear
the thread property.
Now the question is how to associate the window handle with the
object. The place we need to do is in the Analog3dMeterWindowProc
which is the window procedure. The code of the window procedure looks like this
CWnd* GetPointerFromCallContext()
{
IntPtr ip = *dynamic_cast(CallContext::GetData(S"Controls.CurrentControl"));
return (CWnd*)ip.ToPointer();
}
#pragma unmanaged
LRESULT CALLBACK Analog3dMeterWindowProc(HWND hwnd, UINT msg,
WPARAM wp, LPARAM lp)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CWnd* pWnd = CWnd::FromHandlePermanent(hwnd);
if (pWnd == NULL)
{
pWnd = GetPointerFromCallContext();
ASSERT(pWnd != NULL);
pWnd->Attach(hwnd);
}
LRESULT ret = AfxCallWndProc(pWnd, hwnd, msg, wp, lp);
if (msg == WM_NCDESTROY)
delete pWnd;
return ret;
}
In the window procedure we first check to see if the window is already in the
permanent handle map. If not (which is the case when the control gets the
WM_NCCREATE
message), obtain the object pointer from call context, which we set
originally in the CreateHandle
method, and attach the window handle to it. Once
we have done that we call AfxCallWndProc
to do all the hard work of window
message handling. Finally, we delete the object pointer when we get
WM_NCCDESTROY
message.
Looking at the window through spy++ shows certain interesting things :-
Observe that the name of the class in WindowForms10.Analog3dMeter.xxx This is
because windows forms framework superclasses our window class and uses the
superclass to create our control.
There is one small implementation detail which I in the constructor of the
ThreeDMeter2
ThreeDMeter2()
{
m_pCtrl = new C3DMeterCtrl();
SetStyle(ControlStyles::UserPaint, false);
}
The SetStyle(ControlStyles::UserPaint, false)
makes it possible for the
WM_PAINT
messages to be forwarded to the original window proc Analog3dMeterWindowProc
instead of them being handled in the managed code.
The class ThreeDMeter2 demonstrates that way windows forms superclasses an
existing window class. We could have managed to do the something without
creating a superclassed window. The control ThreeDMeter3 demonstrates this.
void DefWndProc(Message* m)
{
if (m_pCtrl)
{
if (!m_pCtrl->GetSafeHwnd())
{
m_pCtrl->Attach((HWND)m->HWnd.ToPointer());
}
m->Result = AfxCallWndProc(m_pCtrl, (HWND)m->HWnd.ToPointer(),
m->Msg, (WPARAM)m->WParam.ToPointer(),
(LPARAM)m->LParam.ToPointer());
}
else
Control::DefWndProc(m);
}
void OnHandleDestroyed(EventArgs* e)
{
if (m_pCtrl)
{
m_pCtrl->Detach();
delete m_pCtrl;
m_pCtrl = NULL;
}
}
In ThreeDMeter3 we don't create any separate window class. We don't overload
CreateHandle
or get_CreateParams
. We let Control do it's default creation.
Instead we load DefWndProc
and OnHandleDestroyed
. DefWndProc
is the function
responsible for forwarding the call to the superclassed window procedure (if
the control superclassed any window or else to the DefWindowProc
function). We
overload that and forward the calls to m_pCtrl using AfxCallWndProc
. Finally we
detach and delete m_pCtrl
in OnHandleDestroyed
function. Even though this
method looks simpler this is not very clean (in my opinion) as there are two
potential routes to DefWindowProc
one through MFC implementation and other
through System::Windows::Forms::Control
's implementation.
Thus, in short I have covered certain ways by which existing MFC controls can be
moved to Windows Forms without completely rewriting them.
My special thanks to Mark C. Malburg for making his code available. My special
thanks to Essam Ahmed for proof reading the article.
Updates
3/13/2002
-
Modified string properties to use CString constructor for System::String*
-
Added ThreeDMeter3